/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * FTPConnection.java
 * Copyright (C) 2011-2019 University of Waikato, Hamilton, New Zealand
 */

package adams.flow.standalone;

import adams.core.PasswordSupporter;
import adams.core.QuickInfoHelper;
import adams.core.base.BasePassword;
import adams.core.io.ConsoleHelper;
import adams.flow.control.Flow;
import adams.flow.core.OptionalPasswordPrompt;
import adams.flow.core.StopHelper;
import adams.flow.core.StopMode;
import adams.gui.dialog.PasswordDialog;
import org.apache.commons.net.ProtocolCommandEvent;
import org.apache.commons.net.ProtocolCommandListener;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import java.awt.Dialog;
import java.awt.Dialog.ModalityType;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

/**
 <!-- globalinfo-start -->
 * Provides access to a FTP host.<br>
 * If debugging is enabled, the FTP commands issued by other actors will get printed as debug output of this actor.
 * <br><br>
 <!-- globalinfo-end -->
 *
 <!-- flow-summary-start -->
 <!-- flow-summary-end -->
 *
 <!-- options-start -->
 * Valid options are: <br><br>
 *
 * <pre>-D &lt;int&gt; (property: debugLevel)
 * &nbsp;&nbsp;&nbsp;The greater the number the more additional info the scheme may output to 
 * &nbsp;&nbsp;&nbsp;the console (0 = off).
 * &nbsp;&nbsp;&nbsp;default: 0
 * &nbsp;&nbsp;&nbsp;minimum: 0
 * </pre>
 *
 * <pre>-name &lt;java.lang.String&gt; (property: name)
 * &nbsp;&nbsp;&nbsp;The name of the actor.
 * &nbsp;&nbsp;&nbsp;default: FTPConnection
 * </pre>
 *
 * <pre>-annotation &lt;adams.core.base.BaseText&gt; (property: annotations)
 * &nbsp;&nbsp;&nbsp;The annotations to attach to this actor.
 * &nbsp;&nbsp;&nbsp;default: 
 * </pre>
 *
 * <pre>-skip (property: skip)
 * &nbsp;&nbsp;&nbsp;If set to true, transformation is skipped and the input token is just forwarded 
 * &nbsp;&nbsp;&nbsp;as it is.
 * </pre>
 *
 * <pre>-stop-flow-on-error (property: stopFlowOnError)
 * &nbsp;&nbsp;&nbsp;If set to true, the flow gets stopped in case this actor encounters an error;
 * &nbsp;&nbsp;&nbsp; useful for critical actors.
 * </pre>
 *
 * <pre>-host &lt;java.lang.String&gt; (property: host)
 * &nbsp;&nbsp;&nbsp;The host (name&#47;IP address) to connect to.
 * &nbsp;&nbsp;&nbsp;default: 
 * </pre>
 *
 * <pre>-user &lt;java.lang.String&gt; (property: user)
 * &nbsp;&nbsp;&nbsp;The FTP user to use for connecting.
 * </pre>
 *
 * <pre>-password &lt;adams.core.base.BasePassword&gt; (property: password)
 * &nbsp;&nbsp;&nbsp;The password of the FTP user to use for connecting.
 * </pre>
 *
 * <pre>-passive (property: usePassiveMode)
 * &nbsp;&nbsp;&nbsp;If enabled, passive mode is used instead.
 * </pre>
 *
 * <pre>-binary (property: useBinaryMode)
 * &nbsp;&nbsp;&nbsp;If enabled, binary mode is used instead of ASCII.
 * </pre>
 *
 * <pre>-prompt-for-password (property: promptForPassword)
 * &nbsp;&nbsp;&nbsp;If enabled, the user gets prompted for enter a password if none has been 
 * &nbsp;&nbsp;&nbsp;provided in the setup.
 * </pre>
 *
 * <pre>-stop-if-canceled (property: stopFlowIfCanceled)
 * &nbsp;&nbsp;&nbsp;If enabled, the flow gets stopped in case the user cancels the dialog.
 * </pre>
 *
 * <pre>-custom-stop-message &lt;java.lang.String&gt; (property: customStopMessage)
 * &nbsp;&nbsp;&nbsp;The custom stop message to use in case a user cancelation stops the flow 
 * &nbsp;&nbsp;&nbsp;(default is the full name of the actor)
 * &nbsp;&nbsp;&nbsp;default: 
 * </pre>
 *
 <!-- options-end -->
 *
 * @author  fracpete (fracpete at waikato dot ac dot nz)
 */
public class FTPConnection
  extends AbstractStandalone
  implements ProtocolCommandListener, OptionalPasswordPrompt, PasswordSupporter {

  /** for serialization. */
  private static final long serialVersionUID = -1959430342987913960L;

  /** the FTP host. */
  protected String m_Host;

  /** the FTP user to use. */
  protected String m_User;

  /** the FTP password to use. */
  protected BasePassword m_Password;

  /** whether to use passive mode. */
  protected boolean m_UsePassiveMode;

  /** whether to use binary file transfer mode. */
  protected boolean m_UseBinaryMode;

  /** the actual SMTP password to use. */
  protected BasePassword m_ActualPassword;

  /** whether to prompt the user for a password if none provided. */
  protected boolean m_PromptForPassword;

  /** whether to stop the flow if canceled. */
  protected boolean m_StopFlowIfCanceled;

  /** how to perform the stop. */
  protected StopMode m_StopMode;

  /** the custom stop message to use if flow gets stopped due to cancelation. */
  protected String m_CustomStopMessage;

  /** the FTP client object. */
  protected FTPClient m_Client;

  /**
   * Returns a string describing the object.
   *
   * @return 			a description suitable for displaying in the gui
   */
  @Override
  public String globalInfo() {
    return
      "Provides access to a FTP host.\n"
        + "If debugging is enabled, the FTP commands issued by other actors will "
        + "get printed as debug output of this actor.";
  }

  /**
   * Adds options to the internal list of options.
   */
  @Override
  public void defineOptions() {
    super.defineOptions();

    m_OptionManager.add(
      "host", "host",
      "");

    m_OptionManager.add(
      "user", "user",
      "anonymous", false);

    m_OptionManager.add(
      "password", "password",
      new BasePassword(""), false);

    m_OptionManager.add(
      "passive", "usePassiveMode",
      false);

    m_OptionManager.add(
      "binary", "useBinaryMode",
      false);

    m_OptionManager.add(
      "prompt-for-password", "promptForPassword",
      false);

    m_OptionManager.add(
      "stop-if-canceled", "stopFlowIfCanceled",
      false);

    m_OptionManager.add(
      "custom-stop-message", "customStopMessage",
      "");

    m_OptionManager.add(
      "stop-mode", "stopMode",
      StopMode.GLOBAL);
  }

  /**
   * Resets the actor.
   */
  @Override
  protected void reset() {
    super.reset();

    disconnect();
  }

  /**
   * Returns a quick info about the actor, which will be displayed in the GUI.
   *
   * @return		null if no info available, otherwise short string
   */
  @Override
  public String getQuickInfo() {
    String		result;
    List<String>	options;
    String		value;

    result  = QuickInfoHelper.toString(this, "user", m_User);
    value = QuickInfoHelper.toString(this, "password", m_Password.getValue().replaceAll(".", "*"));
    if (value != null)
      result += ":" + value;
    value = QuickInfoHelper.toString(this, "host", m_Host);
    if (value != null)
      result += "@" + value;

    options = new ArrayList<String>();
    QuickInfoHelper.add(options, QuickInfoHelper.toString(this, "usePassiveMode", m_UsePassiveMode, "passive"));
    QuickInfoHelper.add(options, QuickInfoHelper.toString(this, "useBinaryMode", m_UseBinaryMode, "binary"));
    if (QuickInfoHelper.hasVariable(this, "promptForPassword") || m_PromptForPassword) {
      QuickInfoHelper.add(options, QuickInfoHelper.toString(this, "promptForPassword", m_PromptForPassword, "prompt for password"));
      QuickInfoHelper.add(options, QuickInfoHelper.toString(this, "stopFlowIfCanceled", m_StopFlowIfCanceled, "stop flow"));
    }
    result += QuickInfoHelper.flatten(options);

    return result;
  }

  /**
   * Sets the host to connect to.
   *
   * @param value	the host name/ip
   */
  public void setHost(String value) {
    m_Host = value;
    reset();
  }

  /**
   * Returns the host to connect to.
   *
   * @return		the host name/ip
   */
  public String getHost() {
    return m_Host;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String hostTipText() {
    return "The host (name/IP address) to connect to.";
  }

  /**
   * Sets the FTP user to use.
   *
   * @param value	the user name
   */
  public void setUser(String value) {
    m_User = value;
    reset();
  }

  /**
   * Returns the FTP user name to use.
   *
   * @return		the user name
   */
  public String getUser() {
    return m_User;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String userTipText() {
    return "The FTP user to use for connecting.";
  }

  /**
   * Sets the FTP password to use.
   *
   * @param value	the password
   */
  public void setPassword(BasePassword value) {
    m_Password = value;
    reset();
  }

  /**
   * Returns the FTP password to use.
   *
   * @return		the password
   */
  public BasePassword getPassword() {
    return m_Password;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String passwordTipText() {
    return "The password of the FTP user to use for connecting.";
  }

  /**
   * Sets whether to use passive mode.
   *
   * @param value	if true passive mode is used
   */
  public void setUsePassiveMode(boolean value) {
    m_UsePassiveMode = value;
    reset();
  }

  /**
   * Returns whether passive mode is used.
   *
   * @return		true if passive mode is used
   */
  public boolean getUsePassiveMode() {
    return m_UsePassiveMode;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String usePassiveModeTipText() {
    return "If enabled, passive mode is used instead.";
  }

  /**
   * Sets whether to use binary mode.
   *
   * @param value	if true binary mode is used
   */
  public void setUseBinaryMode(boolean value) {
    m_UseBinaryMode = value;
    reset();
  }

  /**
   * Returns whether binary mode is used.
   *
   * @return		true if binary mode is used
   */
  public boolean getUseBinaryMode() {
    return m_UseBinaryMode;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String useBinaryModeTipText() {
    return "If enabled, binary mode is used instead of ASCII.";
  }

  /**
   * Sets whether to prompt for a password if none currently provided.
   *
   * @param value	true if to prompt for a password
   */
  public void setPromptForPassword(boolean value) {
    m_PromptForPassword = value;
    reset();
  }

  /**
   * Returns whether to prompt for a password if none currently provided.
   *
   * @return		true if to prompt for a password
   */
  public boolean getPromptForPassword() {
    return m_PromptForPassword;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  public String promptForPasswordTipText() {
    return
      "If enabled, the user gets prompted "
        + "for enter a password if none has been provided in the setup.";
  }

  /**
   * Sets whether to stop the flow if dialog canceled.
   *
   * @param value	if true flow gets stopped if dialog canceled
   */
  public void setStopFlowIfCanceled(boolean value) {
    m_StopFlowIfCanceled = value;
    reset();
  }

  /**
   * Returns whether to stop the flow if dialog canceled.
   *
   * @return 		true if the flow gets stopped if dialog canceled
   */
  public boolean getStopFlowIfCanceled() {
    return m_StopFlowIfCanceled;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return		tip text for this property suitable for
   *             	displaying in the GUI or for listing the options.
   */
  public String stopFlowIfCanceledTipText() {
    return "If enabled, the flow gets stopped in case the user cancels the dialog.";
  }

  /**
   * Sets the custom message to use when stopping the flow.
   *
   * @param value	the stop message
   */
  public void setCustomStopMessage(String value) {
    m_CustomStopMessage = value;
    reset();
  }

  /**
   * Returns the custom message to use when stopping the flow.
   *
   * @return		the stop message
   */
  public String getCustomStopMessage() {
    return m_CustomStopMessage;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return		tip text for this property suitable for
   *             	displaying in the GUI or for listing the options.
   */
  public String customStopMessageTipText() {
    return
      "The custom stop message to use in case a user cancelation stops the "
        + "flow (default is the full name of the actor)";
  }

  /**
   * Sets the stop mode.
   *
   * @param value	the mode
   */
  @Override
  public void setStopMode(StopMode value) {
    m_StopMode = value;
    reset();
  }

  /**
   * Returns the stop mode.
   *
   * @return		the mode
   */
  @Override
  public StopMode getStopMode() {
    return m_StopMode;
  }

  /**
   * Returns the tip text for this property.
   *
   * @return 		tip text for this property suitable for
   * 			displaying in the GUI or for listing the options.
   */
  @Override
  public String stopModeTipText() {
    return "The stop mode to use.";
  }

  /**
   * Performs the interaction with the user.
   *
   * @return		true if successfully interacted
   */
  public boolean doInteract() {
    boolean		result;
    PasswordDialog	dlg;

    dlg = new PasswordDialog((Dialog) null, ModalityType.DOCUMENT_MODAL);
    dlg.setLocationRelativeTo(getParentComponent());
    ((Flow) getRoot()).registerWindow(dlg, dlg.getTitle());
    dlg.setVisible(true);
    ((Flow) getRoot()).deregisterWindow(dlg);
    result = (dlg.getOption() == PasswordDialog.APPROVE_OPTION);

    if (result)
      m_ActualPassword = dlg.getPassword();

    return result;
  }

  /**
   * Returns whether headless interaction is supported.
   *
   * @return		true if interaction in headless environment is possible
   */
  public boolean supportsHeadlessInteraction() {
    return true;
  }

  /**
   * Performs the interaction with the user in a headless environment.
   *
   * @return		true if successfully interacted
   */
  public boolean doInteractHeadless() {
    boolean		result;
    BasePassword	password;

    result   = false;
    password = ConsoleHelper.enterPassword("Please enter password (" + getName() + "):");
    if (password != null) {
      result           = true;
      m_ActualPassword = password;
    }

    return result;
  }

  /**
   * Returns the FTP client object.
   *
   * @return		the FTP client, null if failed to connect
   */
  public synchronized FTPClient getFTPClient() {
    if (m_Client == null)
      connect();

    return m_Client;
  }

  /**
   * Executes the flow item.
   *
   * @return		null if ok, otherwise error message
   */
  @Override
  protected String doExecute() {
    String	result;

    result = null;

    if (m_Client == null) {
      if (isLoggingEnabled())
        getLogger().info("Starting new session");

      m_ActualPassword = m_Password;

      if (m_PromptForPassword && (m_Password.getValue().length() == 0)) {
        if (!isHeadless()) {
          if (!doInteract()) {
            if (m_StopFlowIfCanceled) {
              if ((m_CustomStopMessage == null) || (m_CustomStopMessage.trim().length() == 0))
                StopHelper.stop(this, m_StopMode, "Flow canceled: " + getFullName());
              else
                StopHelper.stop(this, m_StopMode, m_CustomStopMessage);
              result = getStopMessage();
            }
          }
        }
        else if (supportsHeadlessInteraction()) {
          if (!doInteractHeadless()) {
            if (m_StopFlowIfCanceled) {
              if ((m_CustomStopMessage == null) || (m_CustomStopMessage.trim().length() == 0))
                StopHelper.stop(this, m_StopMode, "Flow canceled: " + getFullName());
              else
                StopHelper.stop(this, m_StopMode, m_CustomStopMessage);
              result = getStopMessage();
            }
          }
        }
      }

      if (result == null)
        result = connect();
    }
    else {
      if (isLoggingEnabled())
        getLogger().info("Re-using current session");
    }

    return result;
  }

  /**
   * Starts up a FTP session.
   *
   * @return		null if OK, otherwise error message
   */
  protected String connect() {
    String	result;
    int		reply;

    result = null;

    try {
      m_Client = new FTPClient();
      m_Client.addProtocolCommandListener(this);
      m_Client.connect(m_Host);
      reply = m_Client.getReplyCode();
      if (!FTPReply.isPositiveCompletion(reply)) {
        result = "FTP server refused connection: " + reply;
      }
      else {
        if (!m_Client.login(m_User, m_ActualPassword.getValue())) {
          result = "Failed to connect to '" + m_Host + "' as user '" + m_User + "'";
        }
        else {
          if (m_UsePassiveMode)
            m_Client.enterLocalPassiveMode();
          if (m_UseBinaryMode)
            m_Client.setFileType(FTPClient.BINARY_FILE_TYPE);
        }
      }
    }
    catch (Exception e) {
      result   = handleException("Failed to connect to '" + m_Host + "' as user '" + m_User + "': ", e);
      m_Client = null;
    }

    return result;
  }

  /**
   * Disconnects the FTP session, if necessary.
   */
  public void disconnect() {
    if (m_Client != null) {
      if (m_Client.isConnected()) {
        try {
          m_Client.disconnect();
        }
        catch (Exception e) {
          getLogger().log(Level.SEVERE, "Failed to disconnect from '" + m_Host + "':", e);
        }
        m_Client.removeProtocolCommandListener(this);
      }
    }
    m_Client = null;
  }

  /**
   * Cleans up after the execution has finished.
   */
  @Override
  public void wrapUp() {
    disconnect();
    super.wrapUp();
  }

  /***
   * This method is invoked by a ProtocolCommandEvent source after
   * sending a protocol command to a server.
   *
   * @param event The ProtocolCommandEvent fired.
   */
  public void protocolCommandSent(ProtocolCommandEvent event) {
    if (isLoggingEnabled())
      getLogger().info("cmd sent: " + event.getCommand() + "/" + event.getReplyCode());
    else if (event.getReplyCode() >= 400)
      getLogger().severe("cmd sent: " + event.getCommand() + "/" + event.getReplyCode());
  }

  /***
   * This method is invoked by a ProtocolCommandEvent source after
   * receiving a reply from a server.
   *
   * @param event The ProtocolCommandEvent fired.
   */
  public void protocolReplyReceived(ProtocolCommandEvent event) {
    if (isLoggingEnabled())
      getLogger().info("reply received: " + event.getMessage() + "/" + event.getReplyCode());
    else if (event.getReplyCode() >= 400)
      getLogger().severe("reply received: " + event.getMessage() + "/" + event.getReplyCode());
  }
}
